package org.geoscrape;

import static org.geoscrape.util.HtmlParser.getContent;

import java.io.IOException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;

import org.geoscrape.util.HtmlParser;
import org.geoscrape.util.Tools;
import org.geoscrape.util.WebClient;

/**
 * Utility class that searches for caches by location, owner, or finder in lists
 * returned by the website.
 * 
 */
public class ListSearcher extends Searcher
{
	private boolean aborted;

	public ListSearcher(Login login)
	{
		super(login);
	}

	/**
	 * Find all caches within a given distance of the given location.
	 * 
	 * @param loc
	 *            the location to search at.
	 * @param maxDistance
	 *            the maximum distance, in kilometres.
	 * @param ignoreFoundAndOwn
	 *            if true, caches the user has already found won't be listed.
	 * @return
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesCloseTo(Location loc, double maxDistance, boolean ignoreFoundAndOwn)
			throws IOException, ParseException
	{
		return findCachesCloseTo(loc, maxDistance, ignoreFoundAndOwn, 0, true);
	}

	/**
	 * Find all caches within a given distance of the given location.
	 * 
	 * @param loc
	 *            the location to search at.
	 * @param maxDistance
	 *            the maximum distance, in kilometres.
	 * @param ignoreFoundAndOwn
	 *            if true, caches the user has already found won't be listed.
	 * @param maxCaches
	 *            the maximum number of caches to return.
	 * @param doLocalCopy
	 *            if true, fill in the return value, otherwise return an empty
	 *            list and only use callbacks.
	 * @return
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesCloseTo(Location loc, double maxDistance, boolean ignoreFoundAndOwn, int maxCaches,
			boolean doLocalCopy) throws IOException, ParseException
	{
		List<Cache> res = new ArrayList<Cache>();
		double mileMaxDistance = maxDistance / 1.609344;
		String queryString = "http://www.geocaching.com/seek/nearest.aspx?";
		queryString += "lat=" + loc.getLatitude().getDegreeWithFraction();
		queryString += "&lng=" + loc.getLongitude().getDegreeWithFraction();
		queryString += "&dist=" + mileMaxDistance;
		if (ignoreFoundAndOwn)
		{
			queryString += "&f=1";
		}
		doSearch(res, queryString, maxCaches, doLocalCopy);

		return res;
	}

	/**
	 * Find all caches within a given distance of the given location.
	 * 
	 * @param loc
	 * @param maxDistance
	 *            the maximum distance, in kilometres.
	 * @return
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesCloseTo(Location loc, double maxDistance) throws IOException, ParseException
	{
		return findCachesCloseTo(loc, maxDistance, false);
	}

	private void doSearch(List<Cache> res, String queryString, int maxCaches, boolean doLocalSave) throws IOException,
			ParseException
	{

		WebClient client = login.getClient();
		doRandomDelay();
		client.setRequestMethod("GET");
		client.goTo(queryString);
		int foundSoFar = 0;

		String content = client.getContentsAsString().trim();
		String mainContent = getContent("<div id=\"ctl00_ContentBody_ResultsPanel\">", content);
		// check if the user has ever found any caches
		if (mainContent != null)
		{
			String recordCountString = getContent("<td class=\"PageBuilderWidget\">", "</b>", mainContent);
			recordCountString = getContent("<b>", recordCountString);
			if (recordCountString != null)
			{
				int totalRecords = Integer.parseInt(recordCountString);
				announceCount(totalRecords);
			}
			String pageCountString = getContent("<td class=\"PageBuilderWidget\">", mainContent);
			pageCountString = getContent("<b>", pageCountString);
			String pageCountString1 = getContent("<b>", "</b>", pageCountString);
			pageCountString = getContent("<b>", pageCountString);
			String pageCountString2 = getContent("<b>", "</b>", pageCountString);
			int totalPages = 1;
			if (pageCountString1 != null && pageCountString2 != null)
			{
				totalPages = Math.max(Integer.parseInt(pageCountString1), Integer.parseInt(pageCountString2));
			}
			foundSoFar = parseSearchResultPage(content, res, maxCaches, doLocalSave, foundSoFar);
			if (aborted)
			{
				return;
			}
			if (maxCaches != 0 && foundSoFar >= maxCaches)
			{
				while (res.size() > maxCaches)
				{
					res.remove(res.size() - 1);
				}
				return;
			}

			for (int x = 2; x <= totalPages; x++)
			{
				HashMap<String, String> inputs = HtmlParser.getViewState(content);
				inputs.put("__EVENTTARGET", "ctl00$ContentBody$pgrTop$ctl08");
				doRandomDelay();
				client.setRequestMethod("POST");
				client.submitForm(queryString, inputs);
				content = client.getContentsAsString().trim();
				foundSoFar = parseSearchResultPage(content, res, maxCaches, doLocalSave, foundSoFar);
				if (aborted)
				{
					return;
				}
				if (maxCaches != 0 && foundSoFar >= maxCaches)
				{
					while (res.size() > maxCaches)
					{
						res.remove(res.size() - 1);
					}
					return;
				}
			}
		}
	}

	/**
	 * Get the caches described in a search result page.
	 * 
	 * @param content
	 * @param res
	 *            list of found caches, if doLocalSave is true - otherwise
	 *            empty.
	 * @param doLocalSave
	 *            if true, save found cacehs to res.
	 * @param foundSoFar
	 *            the number of caches found so far.
	 * 
	 * @throws IOException
	 * @throws ParseException
	 * @return the number of found caches
	 */
	private int parseSearchResultPage(String content, List<Cache> res, int maxCaches, boolean doLocalSave,
			int foundSoFar) throws IOException, ParseException
	{
		content = HtmlParser.convertHtmlChar(content);
		String resultTable = getContent("<table class=\"SearchResultsTable Table\">", "</table>", content);
		if (resultTable == null)
		{
			return foundSoFar;
		}
		int foundCount = foundSoFar;
		List<String> tableRows = HtmlParser.getAllInstances("<tr class=", "</tr>", resultTable);
		for (String row : tableRows)
		{
			List<String> tds = HtmlParser.getAllInstances("<td", "</td>", row);
			if (tds.size() == 11)
			{
				Cache c = new Cache();
				// get the favourite count
				String favouriteCell = tds.get(2);
				String favouriteContent = getContent("title=\"", " ", favouriteCell);
				favouriteContent = favouriteContent.replaceAll("\"", "");
				favouriteContent = favouriteContent.replaceAll("\\.", "");
				favouriteContent = favouriteContent.replaceAll(",", "");
				c.setFavourited(Integer.parseInt(favouriteContent));

				// get the found status
				String foundCell = tds.get(3);
				if (foundCell != null && foundCell.contains("uxFoundStatusIcon") && !foundCell.contains("placed.png"))
				{
					c.setFoundByUser(true);
				}

				// get the cache type
				String typeCell = tds.get(4);
				String typeImage = getContent("<img src=\"http://www.geocaching.com/images/wpttypes/", ".gif", typeCell);
				c.setCacheType(typeImage);

				// get the cache name, url, type, owner and code.
				String mainCell = tds.get(5);
				if (mainCell.contains("class=\"lnk  Strike\">"))
				{
					c.setDisabled(true);
				}
				c.setShortCutURL(getContent("<a href=\"", "\"", mainCell));
				c.setName(getContent("<span>", "</span>", mainCell));

				String details = getContent("<span class=\"small\">", "</span>", mainCell).trim();
				String[] detail = details.split("\\|");
				c.setOwner(new Cacher(detail[0].trim().replace("by ", "")));
				c.setCacheCode(detail[1].trim());

				// check if the "needs maintenance" attribute is set
				String attributeCell = tds.get(6);
				if (attributeCell.contains("/images/icons/16/maintenance.png"))
				{
					c.getAttributes().add(Attribute.FIRSTAID_YES);
				}
				// check if this cache has trackables
				String tbCount = getContent("data-tbcount=\"", "\"", attributeCell);
				if (tbCount != null)
				{
					int trackables = Integer.parseInt(tbCount);
					c.setTrackableCount(trackables);
				}

				// check if this is a premium-only cache
				if (attributeCell.contains("/images/icons/16/premium_only.png"))
				{
					c.setPremiumonly(true);
				}

				// get the size, difficulty, and terrain
				String diffTerrSizeDesc = tds.get(7);
				if (diffTerrSizeDesc != null)
				{
					// get difficulty
					String diff = getContent("<span class=\"small\">", "/", diffTerrSizeDesc);
					if (diff != null)
					{
						c.setDifficultyRating(Double.parseDouble(diff));
					}
					// get terrain
					String terr = getContent("/", "</span>", diffTerrSizeDesc);
					if (terr != null)
					{
						c.setTerrainRating(Double.parseDouble(terr));
					}
					// get size
					String container = getContent("/images/icons/container/", ".gif", diffTerrSizeDesc);
					if (container != null)
					{
						if (container.equals("micro"))
						{
							c.setCacheSize(CacheSize.MICRO);
						}
						else if (container.equals("small"))
						{
							c.setCacheSize(CacheSize.SMALL);
						}
						else if (container.equals("regular"))
						{
							c.setCacheSize(CacheSize.REGULAR);
						}
						else if (container.equals("large"))
						{
							c.setCacheSize(CacheSize.LARGE);
						}
						else if (container.equals("other"))
						{
							c.setCacheSize(CacheSize.OTHER);
						}
						else if (container.equals("virtual"))
						{
							c.setCacheSize(CacheSize.VIRTUAL);
						}
						else if (container.equals("nano"))
						{
							c.setCacheSize(CacheSize.NANO);
						}
						else if (container.equals("not_chosen"))
						{
							c.setCacheSize(CacheSize.NOT_CHOSEN);
						}
						else
						{
							c.setCacheSize(CacheSize.UKNOWN);
						}
					}
				}

				// get the placed date
				SimpleDateFormat df = new SimpleDateFormat(login.getDateFormat());
				String hiddenDate = getContent("<span class=\"small\">", "<", tds.get(8)).trim();
				Long time = getParsedFancyTime(hiddenDate, df);
				c.setPlacedAt(time);

				// get the last found date
				String lastFoundDate = getContent("<span class=\"small\">", "<", tds.get(9)).trim();
				time = getParsedFancyTime(lastFoundDate, df);
				c.setLastFoundDate(time);

				announceCache(c);
				if(doLocalSave)
				{
					res.add(c);
				}
				foundCount++;
				if (maxCaches > 0 && foundCount >= maxCaches)
				{
					break;
				}
				if (aborted)
				{
					break;
				}
			}
		}
		return foundCount;
	}

	/**
	 * Parse the hidden/found dates from the list view.
	 * 
	 * @param date
	 * @param df
	 * @return
	 */
	private Long getParsedFancyTime(String date, SimpleDateFormat df)
	{
		Long res = null;
		try
		{
			Date d = df.parse(date);
			res = d.getTime();
		}
		catch (ParseException e)
		{
			if (Tools.isToday(date))
			{
				res = new Date().getTime();
			}
			else if (Tools.isYesterday(date))
			{
				Calendar c = new GregorianCalendar();
				c.add(Calendar.DATE, -1);
				res = c.getTimeInMillis();
			}
			else
			{
				// try to remove anything non-digit from date, and parse that
				String tmp = "";
				for (int x = 0; x < date.length(); x++)
				{
					if (Character.isDigit(date.charAt(x)))
					{
						tmp += date.charAt(x);
					}
				}
				try
				{
					int daysAgo = Integer.parseInt(tmp);
					Calendar c = new GregorianCalendar();
					c.add(Calendar.DATE, -daysAgo);
					res = c.getTimeInMillis();
				}
				catch (NumberFormatException e2)
				{

				}
				if (res == null && date != null && date.length() > 0)
				{
					System.out.println("Don't know how to parse date \"" + date + "\"");
				}
			}

		}
		return res;
	}

	/**
	 * Find all caches owned by a certain cacher.
	 * 
	 * @param owner
	 *            the owner of the caches.
	 * @return an array list of caches.
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesOwnedBy(Cacher owner, int maxCaches) throws IOException, ParseException
	{
		return findCachesOwnedBy(owner, maxCaches, true);
	}

	/**
	 * Find all caches owned by a certain cacher.
	 * 
	 * @param owner
	 *            the owner of the caches.
	 * @param doLocalCopy
	 *            if true, the return value will contain a list of found caches.
	 * @return an array list of caches.
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesOwnedBy(Cacher owner, int maxCaches, boolean doLocalCopy) throws IOException,
			ParseException
	{
		ArrayList<Cache> res = new ArrayList<Cache>();
		String queryString = "http://www.geocaching.com/seek/nearest.aspx?u=";
		String queryEnd = "&submit4=Go";
		String name = URLEncoder.encode(owner.getName(), "UTF-8");
		String query = queryString + name + queryEnd;
		doSearch(res, query, maxCaches, doLocalCopy);
		return res;
	}

	/**
	 * Find all caches found by a certain cacher.
	 * 
	 * @param finder
	 *            the finder of the caches.
	 * @return an array list of caches.
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesFoundBy(Cacher finder) throws IOException, ParseException
	{
		return findCachesFoundBy(finder, 0);
	}

	/**
	 * Find latest maxCaches caches found by a certain cacher.
	 * 
	 * @param finder
	 *            the finder of the caches.
	 * @param maxCaches
	 *            the maximum number of caches to return, return all caches if
	 *            maxCaches = 0.
	 * @return an array list of caches.
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesFoundBy(Cacher finder, int maxCaches) throws IOException, ParseException
	{
		return findCachesFoundBy(finder, maxCaches, true);
	}

	/**
	 * Find latest maxCaches caches found by a certain cacher.
	 * 
	 * @param finder
	 *            the finder of the caches.
	 * @param maxCaches
	 *            the maximum number of caches to return, return all caches if
	 *            maxCaches = 0.
	 * @param doLocalCopy
	 *            if true, the return value will contain the list of found
	 *            caches.
	 * @return an array list of caches.
	 * @throws IOException
	 * @throws ParseException
	 */
	public List<Cache> findCachesFoundBy(Cacher finder, int maxCaches, boolean doLocalCopy) throws IOException,
			ParseException
	{
		ArrayList<Cache> res = new ArrayList<Cache>();
		String queryString = "http://www.geocaching.com/seek/nearest.aspx?ul=";
		String queryEnd = "&submit4=Go";
		String name = finder.getName();
		name = URLEncoder.encode(name, "UTF-8");
		String query = queryString + name + queryEnd;
		doSearch(res, query, maxCaches, doLocalCopy);
		return res;
	}

	/**
	 * Stop the search. The findCaches.... method will return.
	 */
	public void abort()
	{
		this.aborted = true;
	}
}
